#include <iostream>       // 提供“输入输出流操作” ---> cout、cerr
#include <unistd.h>       // 提供“系统调用” ---> pipe、fork、write、read、sleep 等

#include <sys/types.h>    // 提供 pid_t 等类型定义
#include <sys/wait.h>     // 提供 waitpid 函数（等待子进程结束）
#include <cstdio>         // 提供 snprintf 函数（格式化字符串到缓冲区）
#include <cstring>        // 提供 strlen 函数（计算字符串长度）

/*----------------------------------------------------- 辅助函数 -----------------------------------------------------*/
//实现：子进程执行的写操作函数：向管道写端写入数据
void ChildWrite(int wfd) //注意：参数 wfd：管道的写端文件描述符
{
    //1.定义缓冲区，用于存放待写入的数据
    char buffer[1024];    
    //2.定义计数器，用于标识消息序号
    int cnt = 0;          

    //3.无限循环，持续向管道写入数据
    while (true)          
    {
        //3.1：格式化字符串：包含子进程 PID 和计数器值，存入缓冲区
        snprintf(buffer, sizeof(buffer), "我是子进程, pid: %d, cnt: %d", getpid(), cnt++);

        //3.2：向管道写端写入数据（仅写入有效字符串长度，不含末尾空字符）
        write(wfd, buffer, strlen(buffer));

        //3.3：休眠 1 秒，控制写入频率
        sleep(1);         
    }
}

//实现：父进程执行的读操作函数：从管道读端读取数据
void FatherRead(int rfd) //注意：参数 rfd：管道的读端文件描述符
{
    //1.定义缓冲区，用于存放读取到的数据
    char buffer[1024];    

    //2.无限循环，持续从管道读取数据
    while (true)          
    {
        //2.1：清空缓冲区，避免残留上次读取的数据
        buffer[0] = 0;    

        //2.2：从管道读端读取数据，最多读取 sizeof(buffer)-1 字节（预留 1 字节存空字符）
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        /* 注意细节：这里read的第三个参数一定要预留一个字节的空间
        *  这是因为：
        *      1. 当我们向一个文件写入一个字符串的时候是不用将这个字符串末尾的\0也写到文件中的            
        *      2. 因为字符串以\0结尾这是C语言的标准，文件中并没有这个种要求
        *      3. 所以上面我们使用系统调用write将字符串buffer中的内容写到文件中的时候使用的是strlen(buffer) 指定写的长度
        *      4. 就是为了不将字符串的\0结束标志写到文件中
        * 
        *      5. 从文件中读字符串的时候，使用sizeof执定读的字符串长度，也就是为了预留一个字节的空间
        *      6. 方便我们后面手动添加字符串的结束标志\0
        *  
        * 
        * 
        * 深挖细节：为什么snprintf(buffer, sizeof(buffer), "我是子进程, pid: %d, cnt: %d", getpid(), cnt++);
        *          这行代码中我们的写的不是：sizeof(buffer)-1呢？
        * 这是因为： 
        *      1. C标准库中处理字符串的函数会自动的为我们的字符串添加字符串结束标符\0
        *      2. 而系统调用是Linux操作系统提供的，它是不关心C语言的字符串的格式的
        *      3. 所以说就是：执行完库函数snprintf得到的字符串buffer是有\0字符串结束标志的
        *      4. 不需要我们指定字符串的长度为sizeof(buffer)-1，然后再在字符串的末尾手动添加上字符串的结束标志\0
        */

        //2.3：判断读取数据是否失败
        if (n > 0)      //注意：返回值 n 为实际读取的字节数（注：sizeof(buffer) - 1是期望读取的字节的数量），若为 -1 表示读取失败
        {
            buffer[n] = 0; //注意：在读取到的数据末尾添加空字符，确保字符串完整
            std::cout << "子进程说：" << buffer << std::endl; // 打印子进程发送的消息
        }
    }
}

/*----------------------------------------------------- 主程序 -----------------------------------------------------*/
int main()
{
    /*------------------------------------第一步：创建匿名管道------------------------------------*/
    //1.定义数组fds用于存储管道的两个文件描述符
    int fds[2] = {0};   
    //2.调用pipe系统调用创建管道 ---> 成功返回 0，失败返回 -1
    int n = pipe(fds); 
    //3.判断管道创建是否失败
    if (n < 0)      
    {
        std::cerr << "pipe error" << std::endl; 
        return 1;        
    }

    //4.打印管道的读端和写端文件描述符（通常从 3 开始，因 0、1、2 被标准输入/输出/错误占用）
    std::cout << "fds[0]: " << fds[0] << std::endl;
    std::cout << "fds[1]: " << fds[1] << std::endl;

    /*------------------------------------第二步：创建子进程------------------------------------*/
    //1.调用fork系统调用常见子进程 ---> 父进程返回子进程 PID，子进程返回 0，失败返回 -1
    pid_t id = fork(); 

    /*------------------------------------第三步：关闭无关端 ---> 父进程读、子进程写------------------------------------*/
    //2.子进程的执行分支
    if (id == 0)          
    {
        //2.1：关闭子进程不需要的管道读端(fds[0])，构建单向通信信道
        close(fds[0]);

        //2.2：调用子进程写函数，传入管道写端文件描述符
        ChildWrite(fds[1]);

        //2.3：写操作结束后关闭写端
        close(fds[1]);

        //2.4：子进程退出
        exit(0);         
    }

    //3.父进程的执行分支
    //3.1：关闭子进程不需要的管道写端(fds[1])，构建单向通信信道
    close(fds[1]);

    //3.2：调用父进程读函数，传入管道读端文件描述符
    FatherRead(fds[0]);

    //3.3：等待子进程结束
    waitpid(id, nullptr, 0);  //注意：“父进程等待子进程结束” 要在 “父进程读函数” 之后调用

    //3.4：读操作结束后关闭读端
    close(fds[0]);

    return 0;            
}